iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 13
0
Modern Web

Nest.js framework 30天初探系列 第 13

Nestjs framework 30天初探:Day13 WebSocket-Socket.IO聊天室(part2)

  • 分享至 

  • xImage
  •  

Socket.IO+Guard

Guard在day08,有初步介紹過,Guard機制不只可以用在Route上,也可以用在WebSocket Gateway上。
我們先實現Route Guard複習一下,在本篇會使用session存放user object,裏頭key有account、roles,讓route guard機制去驗證,然後也新增一個頁面作為加入聊天室使用,要訪問聊天室網頁必須先通過route guard機制。

  1. 請裝express-session
    cmd指令
 npm install --save express-session @types/express-session
  1. 修改server.ts,加入express-session模組。
    src/server.ts
    部分程式碼
//使用session
instance.use(session({
  secret: 'nestjs session',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}))
  1. view新增toAddInChatRoom.ejs、並修改一下chatRoom.ejs。
    src/views/Chat/toAddInChatRoom.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <label>加入聊天室<label>
    <form method="POST" action="/addInChatRoom">
        <label>名字:<label>
        <input name="Account">
        <input type="submit" value="加入">
    </form>
</body>
</html>  

src/view/Chat/chatRoom.ejs
script部分

<script>   
        const socket = io('ws://localhost:81/messages');
        $('#chatForm').submit(function(){
            //推訊息
            let Message=getCookie('name')+":"+$('#chatMessage').val();
            console.log(Message);
            socket.emit('pushMessage', Message);
            $('#chatMessage').val('');
            return false;
        });
        //監聽新訊息事件
         socket.on('newMessage', function(msg){
            //顯示訊息 
            $('#messages').append($('<li>').text(msg));
            window.scrollTo(0, document.body.scrollHeight);
        });
        //監聽連線事件
        socket.on('connect', function() {
            console.log('Connected');
        });
        //監聽斷線事件
        socket.on('disconnect', function() {
            console.log('Disconnected');
        });
        
        //取得cookie 值
        function getCookie(name) {
            var value = "; " + document.cookie;
            var parts = value.split("; " + name + "=");
            if (parts.length == 2) return parts.pop().split(";").shift();
        } 
    </script>
  1. 修改ChatController程式碼。
    src/modules/Chat/chat.controller.ts
import { Controller, Get, Post, Request, Response, Next, HttpStatus, UseGuards } from '@nestjs/common';
import { RolesGuard } from '../Shared/Guards/roles.guard';
import { Roles } from '../Shared/decorators/roles.decorator';

@Controller()
@UseGuards(RolesGuard)
export class ChatController {

    constructor() { }

    @Get('toAddInChatRoom')
    //使用Express的參數
    async toAddInChatRoom( @Request() req, @Response() res, @Next() next) {
        //跟expressjs專案一樣,指定view路徑,後面帶變數可以直接render到view上
        res.render('./Chat/toAddInChatRoom', { title: "加入聊天室" });
    }

    @Post('addInChatRoom')
    //使用Express的參數
    async addInChatRoom( @Request() req, @Response() res, @Next() next) {
        /*
        1.以下是要建立路由警衛機制,刻意給每個有輸入名稱的使用者一個role,
        並且為了在聊天室顯示使用者名稱,透過cookie存放輸入名稱,這邊只是為了demo,
        實際存放名稱要考量更多資安問題。
        2.我們透過session存放account和role,在Guard機制裏頭,我們會去比對role。
         */
        let tmpAccount: string = req.body.Account;
        req.session.user={};
        req.session.user.account = tmpAccount;
        if (tmpAccount) {
            //role給予general角色
            req.session.user.roles = ["general"];
            //將名稱存放前端cookie,推播訊息時,前端會抓取名稱加上訊息再做推播。
            res.cookie('name', `${tmpAccount}`);
        }
        //跟expressjs專案一樣,指定view路徑,後面帶變數可以直接render到view上
        res.redirect('/chatRoom');
    }


    @Get('chatRoom')
    @Roles('general')
    //使用Express的參數
    async chatRoom( @Request() req, @Response() res, @Next() next) {
        //跟expressjs專案一樣,指定view路徑,後面帶變數可以直接render到view上
        res.render('./Chat/chatRoom', { title: "聊天室" });
    }
}
  1. 修改ChatGateway,程式碼如下。
    src/modules/Chat/chat.gateway.ts
import { WebSocketGateway, SubscribeMessage, WsResponse, WebSocketServer, WsException, NestGateway } from '@nestjs/websockets';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';
import { CreateUserDTO } from '../Users/DTO/create-users.dto';
import Socket = SocketIO.Socket;

//WebSocket listen  port 81,namespace:messages
@WebSocketGateway({ port: 81, namespace: 'messages' })
export class ChatGateway implements NestGateway {
    //使用Socket.IO的API
    socket: Socket;
    constructor() { }

    afterInit(server) { }

    handleConnection(socket) { }

    handleDisconnect(socket) { }
    //新增訊息
    @SubscribeMessage({ value: 'pushMessage' })
    AddMessage(sender, message: string) {
        //推訊息給自己的前端畫面。
        let tmpMessage: string = `${message}`;
        sender.emit('newMessage', tmpMessage);
        //推訊息給其他已建立連線的前端畫面。
        sender.broadcast.emit('newMessage', tmpMessage);
    }
}
  1. 別忘了RolesGuard,在day08,我們是以假資料 req.user = { "account": "Ted", "roles": ["general"] };來實驗我們的Route Guard機制,現在改用session裏頭的user object資料做測試。
    src/modules/Shared/Guards/roles.guard.ts

import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { Reflector } from '@nestjs/core';

@Guard()
export class RolesGuard implements CanActivate {
    constructor(private readonly reflector: Reflector) { }

    canActivate(req, context: ExecutionContext): boolean {
        const { parent, handler } = context;
        const roles = this.reflector.get<string[]>('roles', handler);
        if (!roles) {
            return true;
        }
        /*req.user是假資料,這是在模擬登入後,有一組user資訊放在req object裡,
        也可以放在session等,登入資訊的roles表示角色權限,是陣列,一個帳號可能有多個角色。
        而Ted的角色是general,能夠請求通過帶有@Roles('general')裝飾器的目標。
        */
        //req.user = { "account": "Ted", "roles": ["general"] };
        const user = req.session.user;
        const hasRole = () => !!user.roles.find((role) => !!roles.find((item) => item === role));
        return user && user.roles && hasRole();
    }
}
  1. 實際測試一下我們在聊天室方面的Route Guard機制有沒有建立成功,請打開http://localhost:3000/chatRoom
    結果如下:
    https://ithelp.ithome.com.tw/upload/images/20171217/20107195NzjivCq8IQ.png

符合我們的預期,要直接看到聊天室必須先到加入聊天室的網頁,加入後才能看到。

  1. 所以先去http://localhost:3000/toAddInChatRoom ,輸入名字。
    結果如下:
    https://ithelp.ithome.com.tw/upload/images/20171217/20107195vdMUwBQcgN.png

Route Guard有發揮功用。

9.來玩玩WebSocket Gateway上的Guard機制,新增websocket.roles.guard.ts。
src/modules/Shared/Guards/websocket.roles.guard.ts

import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { Reflector } from '@nestjs/core';

@Guard()
export class WebSocketRolesGuard implements CanActivate {
    constructor(private readonly reflector: Reflector) { }

    canActivate(data, context: ExecutionContext): boolean {
        const { parent, handler } = context;
        const roles = this.reflector.get<string[]>('roles', handler);
        if (!roles) {
            return true;
        }
        /*
        跟Route Guard有點不一樣,req改成data,data泛指傳遞過來的訊息。
        為了 demo用,data是前端push過來的訊息,裏頭有roles 陣列,放著該前端使用者的角色。
        */
        const hasRole = () => !!data.roles.find((role) => !!roles.find((item) => item === role));
        return data && data.roles && hasRole();
    }
}
  1. 修改ChatGatway,程式碼如下。
    src/modules/Chat/chat.gateway.ts
import { WebSocketGateway, SubscribeMessage, WsResponse, WebSocketServer, WsException, NestGateway } from '@nestjs/websockets';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';
import { CreateUserDTO } from '../Users/DTO/create-users.dto';
import Socket = SocketIO.Socket;
import { UseGuards } from '@nestjs/common';
import { Roles } from '../Shared/decorators/roles.decorator';
import { WebSocketRolesGuard } from '../Shared/Guards/webSocket.roles.guard';

//WebSocket listen  port 81,namespace:messages
@WebSocketGateway({ port: 81, namespace: 'messages' })
//Gateway Guard
@UseGuards(WebSocketRolesGuard)
export class ChatGateway implements NestGateway {
    //使用Socket.IO的API
    socket: Socket;
    constructor() { }

    afterInit(server) { }

    handleConnection(socket) { }

    handleDisconnect(socket) { }
    //新增訊息
    @SubscribeMessage({ value: 'pushMessage' })
    @Roles('general')
    AddMessage(sender, message: object) {
        //推訊息給自己的前端畫面。
        sender.emit('newMessage', message);
        //推訊息給其他已建立連線的前端畫面。
        sender.broadcast.emit('newMessage', message);
    }
}
  1. 修改chatRoom.ejs,程式碼如下。
    src/views/Chat/chatRoom.ejs
    script部分。
<script>   
        const socket = io('ws://localhost:81/messages');
        $('#chatForm').submit(function(){
            //推訊息
            var data={};
            var Message=getCookie('name')+":"+$('#chatMessage').val();
            //demo Gateway Guard機制,直接給定角色
            data.roles=['general'];
            data.message=Message;
            socket.emit('pushMessage', data);
            $('#chatMessage').val('');
            return false;
        });
        //監聽新訊息事件
         socket.on('newMessage', function(msg){
            //顯示訊息 
            $('#messages').append($('<li>').text(msg.message));
            window.scrollTo(0, document.body.scrollHeight);
        });
        //監聽連線事件
        socket.on('connect', function() {
            console.log('Connected');
        });
        //監聽斷線事件
        socket.on('disconnect', function() {
            console.log('Disconnected');
        });
        
        //取得cookie 值
        function getCookie(name) {
            var value = "; " + document.cookie;
            var parts = value.split("; " + name + "=");
            if (parts.length == 2) return parts.pop().split(";").shift();
        } 
    </script>

推播訊息原本是string改成object,讓Gateway Guard方便抓角色屬性的值。

12.1 測試一下Gateway Guard,在http://localhost:3000/chatRoom ,輸入訊息,有正常顯示訊息。
https://ithelp.ithome.com.tw/upload/images/20171217/201071950QcZ0pS6g9.png
12.2 接著將chatRoom.ejs網頁的data.roles=['general'];改成data.roles=['wrong role'];,在http://localhost:3000/chatRoom ,網頁重新整理後,再一次輸入訊息就無法顯示訊息了。
https://ithelp.ithome.com.tw/upload/images/20171217/20107195Bqru6gjh2p.png

Gateway Guard有正常發揮功用,對整體網站的保護又多一層防範。Nestjs的Exception Filters、Pipes、Guards、Interceptors、Adapter都能套用在WebSocket的Gateway上,不過我沒太多時間一一去實現demo,這點抱歉了,日後可以繼續關注我的github,我會主攻Nestjs,將Nestjs所有API都玩遍。

程式碼都在github


上一篇
Nestjs framework 30天初探:Day12 WebSocket-Socket.IO聊天室
下一篇
Nestjs framework 30天初探:Day14 MicroServices
系列文
Nest.js framework 30天初探30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言